iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 22
0
自我挑戰組

寫遊戲初體驗系列 第 22

Day 22 Component Pattern

  • 分享至 

  • xImage
  •  

Component Pattern

前言

之前稍微提了一下一般遊戲物件及行為的架構,繼承的部分我就不寫了,各位有興趣可以自己用滿滿的繼承寫一個遊戲,這裡我們要演示的是Component Pattern

Component Pattern的相關知識是從這本書(Game Programming Pattern)學來的,基本上這本書裡講了很多在遊戲設計上,可以很好解決問題的方式,有些我覺得甚至不會限於遊戲設計上,在其他領域也可以用。

有點離題了,我打算用Component Pattern寫一個小小的demo,大概就是一堆物件由上往下掉這麼簡單,至於這個物件呢,由於我很喜歡沙奈朵,就決定放成千上萬個沙奈朵在螢幕上了!

這裡的render部份我不會用到openGL,因為我主要想講的是架構的部分,若想要用前幾天寫好的Renderer2D也是可以的喔。

開始吧

我還只是個菜鳥,經驗還不夠,一定有更好的寫法。

我們最終目標是成千上萬個沙奈朵往下掉,那我們就先想辦法讓一隻沙奈朵往下掉。

我在Info提過,我們會有個Entity,裡面存著Component

所以呢,我們就先寫一個空殼Entity

class Entity {

public:

    Entity();
    
    void update(float dt);
    void render(sf::RenderTarget& target);
};

身為一個遊戲物件,有一個update是很稀鬆平常的事,至於這個render嗎,事後想想把它寫成一個component好像比較好,不過既然我知道我整個螢幕的每個物件都會需要render,就讓我偷懶一下吧XD。

接著我們來想想我們會需要那些component

  • 沙奈朵需要圖片
    • Graphic
  • 圖片產生的位置
    • Transform
  • 圖片掉落速度
    • RigidBody

想好之後就可以開始寫我們的component

在寫之前,因為知道我會需要一些data,所以把他寫進Entity

class Entity {

public:

    float x_, y_;
    float v_;

    Entity();
    
    void update(float dt);
    void render(sf::RenderTarget& target);
};

如果讀了GPP那本書,就會知道我這樣寫不好,更好的方式是將這些data寫進component

接著Component

class Entity;

class Components {

public:
    virtual ~Components() {}
    virtual void update(Entity &entity, float dt) {}
    virtual void render(sf::RenderTarget &target) {}\
};


class Transform: public Components {

public:

    Transform();

    virtual void update(Entity &entity, float dt) override;

};


class RigidBody: public Components {

public:

    virtual void update(Entity &entity, float dt) override;
};


class Graphic: public Components {

public:
    
    Graphic(std::string filePath);
    virtual void update(Entity &entity, float dt) override;
    virtual void render(sf::RenderTarget &target) override;

private:

    sf::Texture texture_;
    sf::Sprite sprite_;
};

其實可以不用一層抽象層,不過寫習慣了就不小心也寫進去了XD

接著把他時做就可以了

實作完之後呢,我們回到我們的Entity

class Transform;
class RigidBody;
class Graphic;

class Entity {

public:

    float x, y;
    float v;

    Entity();
    Entity(Transform *transform, RigidBody *rigidBody, Graphic *graphic);

    void update(float dt);
    void render(sf::RenderTarget &target);

private:
    Transform *transform_;
    RigidBody *rigidBody_;
    Graphic *graphic_;
};

有那層抽象層我們可以利用外部代碼決定我們需要哪些不需要哪些Component,不過這裏我們明確知道我們需要哪些Component。

我們在Entity分別存著3個Component的pointer,而我們的update也很簡單

void Entity::update(float dt) {

    transform_->update(*this, dt);
    rigidBody_->update(*this, dt);
    graphic_->update(*this, dt);
}

這樣就完事了? 差不多, 還差一點呢

我們要來產生物件


MAX_ENTITES = 10000;
std::vector<Entity*> v;
for(int i = 0 ; i < MAX_ENTITES ; i++) {

    Entity *tmp = new Entity(new Transform(), new RigidBody(), new Graphic("沙奈朵.png"));
    v.push_back(tmp);
}

這裡出現了問題,我將圖片路徑傳進Graphic,讓他載入圖片,但我這樣寫他會需要載入一萬次圖片,你能接受遊戲開始前要等5分鐘或更久嗎。

答案是不能,絕對不可能。

所以我打算利用這個模式來處理

SFML載入圖片是存在sf::Texture,而能繪製在螢幕上的是sf::Sprite
他們之間的關係就是這樣

簡單來說Texture就是一張圖片,而把這張圖片貼在矩形物件上就成為Sprite,就可以繪製了
既然我要產生一萬個同樣的沙奈朵物件,我要做的不應該是載入一萬張同樣沙奈朵圖片,而是載入一張沙奈朵圖片,然後每個Sprite都利用那張沙那朵去設定。

所以我寫了個沙奈朵的Model


class Gardevoir {

public:

    Gardevoir(std::string path);

    sf::Texture texture;
};

然後將我的Graphic改成這樣

class Graphic: public Components {

public:

    Graphic(Gardevoir *gardevoir);
    virtual void update(Entity &entity, float dt) override;
    virtual void render(sf::RenderTarget &target) override;

private:

    Gardevoir *gardevoir_;
    sf::Sprite sprite_;
};

我在這裡存了沙那朵的pointer,等於我一萬個物件都會指向同一個沙奈朵

我產生物件的方式就會變這樣

std::vector<Entity*> v;

Gardevoir *gardevoir = new Gardevoir("沙奈朵.png");

for(int i = 0 ; i < MAX_ENTITES ; i++) {

    Entity *tmp = new Entity(new Transform(), new RigidBody(), new Graphic(gardevoir));
    v.push_back(tmp);
}

這樣就只需要載入一次沙奈朵圖片,太棒惹。

我們的Gamp loop怎麼進行呢

float dt = 0.0;
while (running) {

    auto startTime = std::chrono::high_resolution_clock::now();

    sf::Event event;
    while (window.pollEvent(event))
    {
        if (event.type == sf::Event::Closed)
            running = false;
    }

    window.clear(bgColor);

    for(int i = 0 ; i < v.size() ; i++)
        v[i]->update(dt);

    for(int i = 0 ; i < v.size() ; i++)
        v[i]->render(window);

    window.display();
    auto stopTime = std::chrono::high_resolution_clock::now();
    dt = std::chrono::duration<float, std::chrono::seconds::period>(stopTime - startTime).count();
    std::cout << dt << "\n";
}

這樣就完成了,雖然我偷懶了很多地方,但就算組件多起來,寫起來還是挺清晰的,我自己本身是很喜歡這個方法的,因為真的很好寫。

接下來要講的ECS是真的非常難,而且那也不算我自己寫出來的,是看著國外教學文寫的XDDDD,寫完真的覺得想出這個架構的人真的是鬼吧。

接下來給大家看看demo,擺在最後是因為要請有密集恐懼症的人趕緊逃離阿


上一篇
Day 21 ECS intro
下一篇
Day 23 Entity Component System
系列文
寫遊戲初體驗30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言